// Copyright (C) 1999-2001 by Jason Hunter <jhunter_AT_acm_DOT_org>.
// All rights reserved.  Use of this class is limited.
// Please see the LICENSE for more information.

package com.baomidou.framework.upload.multipart;

import java.io.FilterInputStream;
import java.io.IOException;

import javax.servlet.ServletInputStream;

/**
 * A <code>PartInputStream</code> filters a <code>ServletInputStream</code>,
 * providing access to a single MIME part contained with in which ends with the
 * boundary specified. It uses buffering to provide maximum performance.
 * <p>
 * Note the <code>readLine</code> method of <code>ServletInputStream</code> has
 * the annoying habit of adding a \r\n to the end of the last line. Since we
 * want a byte-for-byte transfer, we have to cut those chars. This means that we
 * must always maintain at least 2 characters in our buffer to allow us to trim
 * when necessary.
 * 
 * @author Geoff Soutter
 * @author Jason Hunter
 * @version 1.4, 2002/11/01, fix for "unexpected end of part" caused by boundary
 *          newlines split across buffers
 * @version 1.3, 2001/05/21, fix to handle boundaries crossing 64K mark
 * @version 1.2, 2001/02/07, added read(byte[]) implementation for safety
 * @version 1.1, 2000/11/26, fixed available() to never return negative
 * @version 1.0, 2000/10/27, initial revision
 */
public class PartInputStream extends FilterInputStream {
	/** boundary which "ends" the stream */
	private String boundary;

	/** our buffer */
	private byte[] buf = new byte[64 * 1024]; // 64k

	/** number of bytes we've read into the buffer */
	private int count;

	/** current position in the buffer */
	private int pos;

	/** flag that indicates if we have encountered the boundary */
	private boolean eof;

	/**
	 * Creates a <code>PartInputStream</code> which stops at the specified
	 * boundary from a <code>ServletInputStream<code>.
	 * 
	 * @param in
	 *            a servlet input stream.
	 * @param boundary
	 *            the MIME boundary to stop at.
	 */
	PartInputStream(ServletInputStream in, String boundary) throws IOException {
		super(in);
		this.boundary = boundary;
	}

	/**
	 * Fill up our buffer from the underlying input stream, and check for the
	 * boundary that signifies end-of-file. Users of this method must ensure
	 * that they leave exactly 2 characters in the buffer before calling this
	 * method (except the first time), so that we may only use these characters
	 * if a boundary is not found in the first line read.
	 * 
	 * @exception IOException
	 *                if an I/O error occurs.
	 */
	private void fill() throws IOException {
		if (eof) {
			return;
		}

		// as long as we are not just starting up
		if (count > 0) {
			// if the caller left the requisite amount spare in the buffer
			if (count - pos == 2) {
				// copy it back to the start of the buffer
				System.arraycopy(buf, pos, buf, 0, count - pos);
				count -= pos;
				pos = 0;
			} else {
				// should never happen, but just in case
				throw new IllegalStateException("fill() detected illegal buffer state");
			}
		}

		// Try and fill the entire buffer, starting at count, line by line
		// but never read so close to the end that we might split a boundary
		// Thanks to Tony Chu, tony.chu@brio.com, for the -2 suggestion.
		int read = 0;
		int boundaryLength = boundary.length();
		int maxRead = buf.length - boundaryLength - 2; // -2 is for /r/n
		while (count < maxRead) {
			// read a line
			read = ((ServletInputStream) in).readLine(buf, count, buf.length - count);
			// check for eof and boundary
			if (read == -1) {
				throw new IOException("unexpected end of part");
			} else {
				if (read >= boundaryLength) {
					eof = true;
					for (int i = 0; i < boundaryLength; i++) {
						if (boundary.charAt(i) != buf[count + i]) {
							// Not the boundary!
							eof = false;
							break;
						}
					}
					if (eof) {
						break;
					}
				}
			}
			// success
			count += read;
		}
	}

	/**
	 * See the general contract of the <code>read</code> method of
	 * <code>InputStream</code>.
	 * <p>
	 * Returns <code>-1</code> (end of file) when the MIME boundary of this part
	 * is encountered.
	 *
	 * @return the next byte of data, or <code>-1</code> if the end of the
	 *         stream is reached.
	 * @exception IOException
	 *                if an I/O error occurs.
	 */
	public int read() throws IOException {
		if (count - pos <= 2) {
			fill();
			if (count - pos <= 2) {
				return -1;
			}
		}
		return buf[pos++] & 0xff;
	}

	/**
	 * See the general contract of the <code>read</code> method of
	 * <code>InputStream</code>.
	 * <p>
	 * Returns <code>-1</code> (end of file) when the MIME boundary of this part
	 * is encountered.
	 *
	 * @param b
	 *            the buffer into which the data is read.
	 * @return the total number of bytes read into the buffer, or
	 *         <code>-1</code> if there is no more data because the end of the
	 *         stream has been reached.
	 * @exception IOException
	 *                if an I/O error occurs.
	 */
	public int read(byte b[]) throws IOException {
		return read(b, 0, b.length);
	}

	/**
	 * See the general contract of the <code>read</code> method of
	 * <code>InputStream</code>.
	 * <p>
	 * Returns <code>-1</code> (end of file) when the MIME boundary of this part
	 * is encountered.
	 *
	 * @param b
	 *            the buffer into which the data is read.
	 * @param off
	 *            the start offset of the data.
	 * @param len
	 *            the maximum number of bytes read.
	 * @return the total number of bytes read into the buffer, or
	 *         <code>-1</code> if there is no more data because the end of the
	 *         stream has been reached.
	 * @exception IOException
	 *                if an I/O error occurs.
	 */
	public int read(byte b[], int off, int len) throws IOException {
		int total = 0;
		if (len == 0) {
			return 0;
		}

		int avail = count - pos - 2;
		if (avail <= 0) {
			fill();
			avail = count - pos - 2;
			if (avail <= 0) {
				return -1;
			}
		}
		int copy = Math.min(len, avail);
		System.arraycopy(buf, pos, b, off, copy);
		pos += copy;
		total += copy;

		while (total < len) {
			fill();
			avail = count - pos - 2;
			if (avail <= 0) {
				return total;
			}
			copy = Math.min(len - total, avail);
			System.arraycopy(buf, pos, b, off + total, copy);
			pos += copy;
			total += copy;
		}
		return total;
	}

	/**
	 * Returns the number of bytes that can be read from this input stream
	 * without blocking. This is a standard <code>InputStream</code> idiom to
	 * deal with buffering gracefully, and is not same as the length of the part
	 * arriving in this stream.
	 *
	 * @return the number of bytes that can be read from the input stream
	 *         without blocking.
	 * @exception IOException
	 *                if an I/O error occurs.
	 */
	public int available() throws IOException {
		int avail = (count - pos - 2) + in.available();
		// Never return a negative value
		return (avail < 0 ? 0 : avail);
	}

	/**
	 * Closes this input stream and releases any system resources associated
	 * with the stream.
	 * <p>
	 * This method will read any unread data in the MIME part so that the next
	 * part starts an an expected place in the parent <code>InputStream</code>.
	 * Note that if the client code forgets to call this method on error,
	 * <code>MultipartParser</code> will call it automatically if you call
	 * <code>readNextPart()</code>.
	 *
	 * @exception IOException
	 *                if an I/O error occurs.
	 */
	public void close() throws IOException {
		if (!eof) {
			while (read(buf, 0, buf.length) != -1)
				; // do nothing
		}
	}
}
